/*
 * Tulalip ORM Library, a port of the popular ORMLite library with extensions.
 * Copyright (C) 2012 Richard Schilling
 * 
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 * 
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 * 
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

package com.j256.ormlite.android.apptools;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.sql.SQLException;

import android.content.Context;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteDatabase.CursorFactory;
import android.database.sqlite.SQLiteOpenHelper;
import android.database.sqlite.SQLiteStatement;

import com.j256.ormlite.android.AndroidConnectionSource;
import com.j256.ormlite.android.AndroidDatabaseConnection;
import com.j256.ormlite.dao.Dao;
import com.j256.ormlite.dao.DaoManager;
import com.j256.ormlite.dao.RuntimeExceptionDao;
import com.j256.ormlite.logger.Logger;
import com.j256.ormlite.logger.LoggerFactory;
import com.j256.ormlite.support.ConnectionSource;
import com.j256.ormlite.support.DatabaseConnection;
import com.j256.ormlite.table.DatabaseTableConfigLoader;

/**
 * SQLite database open helper which can be extended by your application to help
 * manage when the application needs to create or upgrade its database.
 * 
 * @author kevingalligan, graywatson, rschilling
 */
public abstract class OrmLiteSqliteOpenHelper extends SQLiteOpenHelper {

    protected static Logger logger = LoggerFactory.getLogger(OrmLiteSqliteOpenHelper.class);
    protected AndroidConnectionSource connectionSource = new AndroidConnectionSource(this);
    private volatile boolean isOpen = true;
    private SQLiteStatement tableExistsStatement;

    /**
     * Creates a helper.
     * 
     * @param context the context that manages this helper.
     * @param databasePath the canonical path to the underlying database file.
     * @param factory the factory used to generate cursors for this database.
     * @param databaseVersion the version number used to determine whether or
     *            not an upgrade happens.
     */
    public OrmLiteSqliteOpenHelper(Context context, String databasePath, CursorFactory factory,
            int databaseVersion) {
        super(context, databasePath, factory, databaseVersion);
        logger.trace("{}: constructed connectionSource {}", this, connectionSource);
    }

    /**
     * Same as the other constructor with the addition of a file-id of the table
     * config-file. See {@link OrmLiteConfigUtil} for details.
     * 
     * @param configFileId file-id which probably should be a
     *            R.raw.ormlite_config.txt or some static value.
     */
    public OrmLiteSqliteOpenHelper(Context context, String databasePath, CursorFactory factory,
            int databaseVersion, int configFileId) {
        this(context, databasePath, factory, databaseVersion, openFileId(context, configFileId));

    }

    /**
     * Same as the other constructor with the addition of a config-file. See
     * {@link OrmLiteConfigUtil} for details.
     * 
     * @param configFile Configuration file to be loaded.
     */
    public OrmLiteSqliteOpenHelper(Context context, String databasePath, CursorFactory factory,
            int databaseVersion, File configFile) {
        this(context, databasePath, factory, databaseVersion, openFile(configFile));
    }

    /**
     * Same as the other constructor with the addition of a input stream to the
     * table config-file. See {@link OrmLiteConfigUtil} for details.
     * 
     * @param stream Stream opened to the configuration file to be loaded.
     */
    public OrmLiteSqliteOpenHelper(Context context, String databasePath, CursorFactory factory,
            int databaseVersion, InputStream stream) {
        super(context, databasePath, factory, databaseVersion);
        if (stream == null) {
            return;
        }

        // if a config file-id was specified then load it into the DaoManager
        try {
            BufferedReader reader = new BufferedReader(new InputStreamReader(stream), 4096);
            DaoManager.addCachedDatabaseConfigs(DatabaseTableConfigLoader
                    .loadDatabaseConfigFromReader(reader));
        } catch (SQLException e) {
            throw new IllegalStateException("Could not load object config file", e);
        } finally {
            try {
                // we close the stream here because we may not get a reader
                stream.close();
            } catch (IOException e) {
                // ignore close errors
            }
        }
    }

    /**
     * What to do when your database needs to be created. Usually this entails
     * creating the tables and loading any initial data.
     * <p>
     * <b>NOTE:</b> You should use the connectionSource argument that is passed
     * into this method call or the one returned by getConnectionSource(). If
     * you use your own, a recursive call or other unexpected results may
     * result.
     * </p>
     * 
     * @param database Database being created.
     * @param connectionSource To use get connections to the database to be
     *            created.
     */
    public abstract void onCreate(SQLiteDatabase database, ConnectionSource connectionSource);

    /**
     * What to do when your database needs to be updated. This could mean
     * careful migration of old data to new data. Maybe adding or deleting
     * database columns, etc..
     * <p>
     * <b>NOTE:</b> You should use the connectionSource argument that is passed
     * into this method call or the one returned by getConnectionSource(). If
     * you use your own, a recursive call or other unexpected results may
     * result.
     * </p>
     * 
     * @param database Database being upgraded.
     * @param connectionSource To use get connections to the database to be
     *            updated.
     * @param oldVersion The version of the current database so we can know what
     *            to do to the database.
     * @param newVersion The version that we are upgrading the database to.
     */
    public abstract void onUpgrade(SQLiteDatabase database, ConnectionSource connectionSource,
            int oldVersion, int newVersion);

    /**
     * Get the connection source associated with the helper.
     */
    public ConnectionSource getConnectionSource() {
        if (!isOpen) {
            // we don't throw this exception, but log it for debugging purposes
            logger.warn(new IllegalStateException(),
                    "Getting connectionSource was called after closed");
        }
        return connectionSource;
    }

    /**
     * Satisfies the {@link SQLiteOpenHelper#onCreate(SQLiteDatabase)} interface
     * method.
     */
    @Override
    public final void onCreate(SQLiteDatabase db) {
        ConnectionSource cs = getConnectionSource();
        /*
         * The method is called by Android database helper's get-database calls
         * when Android detects that we need to create or update the database.
         * So we have to use the database argument and save a connection to it
         * on the AndroidConnectionSource, otherwise it will go recursive if the
         * subclass calls getConnectionSource().
         */
        DatabaseConnection conn = cs.getSpecialConnection();
        boolean clearSpecial = false;
        if (conn == null) {
            conn = new AndroidDatabaseConnection(db, true);
            try {
                cs.saveSpecialConnection(conn);
                clearSpecial = true;
            } catch (SQLException e) {
                throw new IllegalStateException("Could not save special connection", e);
            }
        }
        try {
            onCreate(db, cs);
        } finally {
            if (clearSpecial) {
                cs.clearSpecialConnection(conn);
            }
        }
    }

    /**
     * Satisfies the
     * {@link SQLiteOpenHelper#onUpgrade(SQLiteDatabase, int, int)} interface
     * method.
     */
    @Override
    public final void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
        ConnectionSource cs = getConnectionSource();
        /*
         * The method is called by Android database helper's get-database calls
         * when Android detects that we need to create or update the database.
         * So we have to use the database argument and save a connection to it
         * on the AndroidConnectionSource, otherwise it will go recursive if the
         * subclass calls getConnectionSource().
         */
        DatabaseConnection conn = cs.getSpecialConnection();
        boolean clearSpecial = false;
        if (conn == null) {
            conn = new AndroidDatabaseConnection(db, true);
            try {
                cs.saveSpecialConnection(conn);
                clearSpecial = true;
            } catch (SQLException e) {
                throw new IllegalStateException("Could not save special connection", e);
            }
        }
        try {
            onUpgrade(db, cs, oldVersion, newVersion);
        } finally {
            if (clearSpecial) {
                cs.clearSpecialConnection(conn);
            }
        }
    }

    /**
     * Close any open connections.
     */
    @Override
    public void close() {
        super.close();
        connectionSource.close();
        /*
         * We used to set connectionSource to null here but now we just set the
         * closed flag and then log heavily if someone uses getConectionSource()
         * after this point.
         */
        isOpen = false;
    }

    /**
     * Return true if the helper is still open. Once {@link #close()} is called
     * then this will return false.
     */
    public boolean isOpen() {
        return isOpen;
    }

    /**
     * Get a DAO for our class. This uses the {@link DaoManager} to cache the
     * DAO for future gets.
     * <p>
     * NOTE: This routing does not return Dao<T, ID> because of casting issues
     * if we are assigning it to a custom DAO. Grumble.
     * </p>
     */
    public <D extends Dao<T, ?>, T> D getDao(Class<T> clazz) throws SQLException {
        // special reflection fu is now handled internally by create dao calling
        // the database type
        Dao<T, ?> dao = DaoManager.createDao(getConnectionSource(), clazz);
        @SuppressWarnings("unchecked")
        D castDao = (D) dao;
        return castDao;
    }

    /**
     * Get a RuntimeExceptionDao for our class. This uses the {@link DaoManager}
     * to cache the DAO for future gets.
     * <p>
     * NOTE: This routing does not return RuntimeExceptionDao<T, ID> because of
     * casting issues if we are assigning it to a custom DAO. Grumble.
     * </p>
     */
    public <D extends RuntimeExceptionDao<T, ?>, T> D getRuntimeExceptionDao(Class<T> clazz) {
        try {
            Dao<T, ?> dao = getDao(clazz);
            @SuppressWarnings({ "unchecked", "rawtypes" })
            D castDao = (D) new RuntimeExceptionDao(dao);
            return castDao;
        } catch (SQLException e) {
            throw new RuntimeException("Could not create RuntimeExcepitionDao for class " + clazz,
                    e);
        }
    }

    @Override
    public String toString() {
        return getClass().getSimpleName() + "@" + Integer.toHexString(super.hashCode());
    }

    private static InputStream openFileId(Context context, int fileId) {
        InputStream stream = context.getResources().openRawResource(fileId);
        if (stream == null) {
            throw new IllegalStateException("Could not find object config file with id " + fileId);
        }
        return stream;
    }

    private static InputStream openFile(File configFile) {
        try {
            if (configFile == null) {
                return null;
            } else {
                return new FileInputStream(configFile);
            }
        } catch (FileNotFoundException e) {
            throw new IllegalArgumentException("Could not open config file " + configFile, e);
        }
    }

    /**
     * Returns true if the specified table exists in the database. This check is
     * made by interrogating the {@code sqlite_master} table.
     * 
     * @param tableName the name of the table to check for.
     * @return true if the specified table exists
     * @throws IllegalArgumentException if {@code tableName} is {@code null} or
     *             an empty string.
     * @author Richard Schilling (coderroadie@gmail.com)
     */
    public boolean tableExists(String tableName) {
        if (tableName == null)
            throw new IllegalArgumentException("tableName cannot be null");

        SQLiteStatement stmt = getTableExistsStatement();
        stmt.bindString(1, tableName);
        long count = stmt.simpleQueryForLong();

        return count == 1;

    }

    /**
     * Returns the compiled statement singleton that is used to check whether or
     * not a table exists. Calling {@code simpleQueryForLong} on the returned
     * object will return either a 1 (table exists) or a 0 (table does not
     * exist). You must bind the table name to the statement using
     * {@code .bindString()} before executing the statement.
     * 
     * @return a compiled statement that
     * @author Richard Schilling (coderroadie@gmail.com)
     */
    private SQLiteStatement getTableExistsStatement() {
        if (tableExistsStatement == null) {
            SQLiteDatabase db = getReadableDatabase();

            tableExistsStatement = db.compileStatement("SELECT count(*) > 0 FROM "
                    + "sqlite_master where tbl_name = ? and type='table'");
        }

        return tableExistsStatement;

    }
}
